package ar.edu.itba.it.cg.grupo01.impl.shape;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;

import javax.vecmath.Point2f;
import javax.vecmath.Point3f;
import javax.vecmath.Vector3f;

import ar.edu.itba.it.cg.grupo01.api.Intersection;
import ar.edu.itba.it.cg.grupo01.api.Ray;
import ar.edu.itba.it.cg.grupo01.api.shape.Shape;
import ar.edu.itba.it.cg.grupo01.impl.IntersectionFactory;
import ar.edu.itba.it.cg.grupo01.impl.Precision;

public class Plane extends AbstractShape {

	public Vector3f normal;
	public Point3f point;
	
	//vectors for texture mapping
	private Vector3f u;
	private Vector3f v;

	public Plane(Point3f aPoint, Vector3f normal,
			IntersectionFactory intersectionFactory) {
		super(intersectionFactory);
		this.normal = ((Vector3f) normal.clone());
		this.normal.normalize();
		this.point = (Point3f) aPoint.clone();
	}

	public Plane(Point3f aPoint, Point3f bPoint, Point3f cPoint, IntersectionFactory intersectionFactory)
	{
		super(intersectionFactory);
		
		// Given three point, get two co-plannar vector and cross-product them to get the plane normal :)
		Vector3f oneVector = new Vector3f(bPoint.x - aPoint.x, bPoint.y - aPoint.y, bPoint.z - aPoint.z);
		Vector3f otherVector = new Vector3f(cPoint.x - aPoint.x, cPoint.y - aPoint.y, cPoint.z - aPoint.z);
		Vector3f normalVersor = new Vector3f();
		
		normalVersor.cross(oneVector, otherVector);
		normalVersor.normalize();
		
		this.normal = normalVersor;
		this.point = (Point3f) aPoint.clone();
		
	}
	
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((normal == null) ? 0 : normal.hashCode());
		result = prime * result + ((point == null) ? 0 : point.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Plane other = (Plane) obj;
		float angle = other.normal.angle(normal);
		if (Math.abs(angle - Math.PI) < Precision.ANGLE_EPSILON
				|| Math.abs(angle) < Precision.ANGLE_EPSILON) {
			if (this.contains(other.point)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Checks if a point belongs to a plane
	 * 
	 * @param point2
	 * @return
	 */
	private boolean contains(Point3f point2) {
		Vector3f auxVec = new Vector3f(normal);
		float belong = Math.abs(auxVec.dot(new Vector3f(point2))
				+ this.getDistanceToOrigin());
		if (Math.abs(belong) < Precision.EPSILON) {
			return true;
		}
		return false;
	}

	@Override
	public Intersection<? extends Shape> intersects(final Ray ray) {

		Collection<Float> intersections = findIntersections(ray);

		if (intersections.isEmpty()) {
			return intersectionFactory.emptyIntersection();
		}

		List<Point3f> points = new ArrayList<Point3f>();
		for (Float result : intersections) {

			Point3f point = ray.calculatePoint(result);
			points.add(point);
		}
		return intersectionFactory.createIntersection(points,this);
	}

	private Collection<Float> findIntersections(Ray ray) {

		Collection<Float> intersectionPoints = new HashSet<Float>();

		// First compute Pn Rd = Vd
		float vd = this.normal.dot(ray.getDirection());
		if (vd == 0.0)
			// the ray is parallel to the plane and there is no intersection (if
			// ray in in the plane then we ignore it) empty set
			return Collections.emptySet();

		// V0 = -(Pn R0 + D)
		float v0 = -(normal.dot(new Vector3f(ray.getOrigin())) + this
				.getDistanceToOrigin());

		float t = v0 / vd;

		if (t > 0) {
			intersectionPoints.add(t);
			return intersectionPoints;
		} else
			return Collections.emptySet();

	}

	/*
	 * If plane is represented as ax+by+cz+d=0 d is distance to [0,0,0]
	 */
	public float getDistanceToOrigin() {
		Vector3f auxVec = new Vector3f(normal);
		return (-auxVec.dot(new Vector3f(point)));
	}

	@Override
	public boolean containedInAABB(AABB box) {
		float v;
		float[] vert = new float[3];
		float[] vmin = new float[3];
		float[] vmax = new float[3];
		float[] normal = new float[3];
		float[] maxbox = new float[3];

		this.point.get(vert);
		this.normal.get(normal);
		box.getExtent().get(maxbox);

		for (int i = 0; i < 3; i++) {

			v = vert[i];

			if (normal[i] > 0.0) {
				vmin[i] = -maxbox[i] - v;
				vmax[i] = maxbox[i] - v;
			} else {
				vmin[i] = maxbox[i] - v;
				vmax[i] = -maxbox[i] - v;
			}
		}

		if (this.normal.dot(new Vector3f(vmin)) > 0.0f) {
			return false;
		}

		if (Float.compare(this.normal.dot(new Vector3f(vmax)), 0.0f) >= 0) {
			return true;
		}

		return false;
	}

	@Override
	public Point3f getMaxXYZ() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public Point3f getMinXYZ() {
		// TODO Auto-generated method stub
		return null;
	}

	
	public Point3f getCenter() {
		// TODO Auto-generated method stub
		return null;
	}

	
	//TODO CAMBIAR!!!
	
	public Shape getTransformedShape() {
		return this;
	}

	
	public Collection<Point3f> getVertices() {
		return Collections.emptyList();
	}

	@Override
	public Vector3f getNormal(Point3f point) {
		return (Vector3f) normal.clone();
	}

	@Override
	public Point2f getUVCoordinates(Point3f point) {
	
		if(this.u==null || this.v==null)
			calculateUV(point);
		
		Point3f p1=new Point3f(point);
		p1.sub(this.point);
		
		//find projection over u,v
		Vector3f vec=new Vector3f(p1);
		float u=Math.abs(vec.dot(this.u));
		float v=Math.abs(vec.dot(this.v));
		
		u=u-(float)Math.floor(u);
		v=v-(float)Math.floor(v);
		
		return new Point2f(u,v);
	
	}
	private void calculateUV(Point3f point) {
		Point3f p1=new Point3f(point);
		p1.sub(this.point);
		Vector3f vec=new Vector3f(p1);
		
		//vec and otherVec are perpendicular
		Vector3f otherVec=new Vector3f();
		otherVec.cross(vec,this.normal);

		vec.normalize();
		otherVec.normalize();

		this.u=vec;
		this.v=otherVec;
		
	}

	@Override
	public Point3f getUVWCoordinates(Point3f point) {
		Point3f result = (Point3f) point.clone();
		result.sub(point);
		return result;
	}	
	
}
